﻿using Common.Cache;
using Common.Encrypt;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Text;

namespace Common.Web.Authentication
{
    /// <summary>
    /// jwt帮助
    /// </summary>
    public static class JwtTokenHelper
    {
        internal const string TICKET_NAME = "ticket";
        internal const string USERID_NAME = "id";
        internal const string SCOPE_NAME = "scopes";
        internal const string AUD_NAME = "aud";
        internal const string USER_NAME = "name";
        /// <summary>
        /// 获取token
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        public static JwtSecurityToken GetToken(this HttpRequest request)
        {
            if (!request.Headers.TryGetValue("Authorization", out StringValues bearer))
            {
                return null;
            }
            try
            {
                string base64 = bearer.ToString().Substring(JwtBearerDefaults.AuthenticationScheme.Length + 1);
                var handler = new JwtSecurityTokenHandler();
                return handler.ReadJwtToken(base64);
            }
            catch
            {
                return null;
            }
        }
        /// <summary>
        /// 获取token错误信息
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public static TokenError GetTokenError(this JwtSecurityToken token)
        {
            if (token.Issuer != Issuer)
            {
                return new TokenError("Token-Invalid", "incorrect issuer", "无效的token颁发者");
            }
            if (token.Audiences.Count() <= 0)
            {
                return new TokenError("Token-Invalid", "no audience within token", "token无受众");
            }
            if (Audiences != null && !Audiences.Contains(token.Audiences.First()))
            {
                return new TokenError("Token-Invalid", "audience not allowed", "token指定的受众不允许登录本系统");
            }
            var validTo = token.ValidTo.ToLocalTime();
            if (validTo < DateTime.Now)
            {
                return new TokenError("Token-Invalid", "token has expired", "token已过期");
            }
            string userId = token.Claims?.FirstOrDefault(x => x.Type == USERID_NAME)?.Value;
            string appid = token.Claims?.First(x => x.Type == AUD_NAME)?.Value;
            string strNBF = token.Claims?.FirstOrDefault(x => x.Type == "nbf")?.Value;
            DateTime? nbf = null;
            if (!string.IsNullOrEmpty(strNBF))
            {
                nbf = DateTimeOffset.FromUnixTimeSeconds(long.Parse(strNBF)).LocalDateTime;
            }
            if (IsTokenBanned(userId, appid, nbf, out string memo))
            {
                return new TokenError("Token-Invalid", "Kick-Out", memo ?? "踢出登录");
            }
            return null;
        }
        /// <summary>
        /// 将用户踢出登录，写入redis记录保留15天。因为token的有效期为15天，15天以后该用户的所有token都失效，就不再需要redis记录此uid了。
        /// </summary>
        /// <param name="userId">用户ID</param>
        /// <param name="memo">提示信息，401将输出此提示到前端</param>
        /// <param name="nbt">将此时间之前登录的所有token全部禁用，若为空则相当于禁用此用户</param>
        /// <param name="apps">进退出这些app</param>
        public static bool KickoutUser(long userId, string memo, DateTime? nbt, params string[] apps)
        {
            var banToken = new BanTokenData
            {
                NotBefore = nbt,
                Memo = memo,
                Banned = true
            };
            if (apps != null && apps.Any())
            {
                banToken.Apps.AddRange(apps);
            }
            if (Redis == null)
            {
                Console.WriteLine("Redis对象为空，无法将用户踢出登录");
                return false;
            }
            string key = $"{KickoutCachePrefix}:uid:{userId}";
            Memory.Delete(key);
            var val = Redis.Get<BanTokenData>(key);
            if (val != null && val.Apps != null)
            {
                foreach (var item in val.Apps)
                {
                    if (!banToken.Apps.Contains(item))
                    {
                        banToken.Apps.Add(item);
                    }
                }
            }
            return Redis.Add(key, banToken, TimeSpan.FromDays(15));
        }
        /// <summary>
        /// 批量踢出用户
        /// </summary>
        /// <param name="userIdCollection"></param>
        /// <param name="memo"></param>
        /// <param name="nbt"></param>
        /// <param name="apps"></param>
        /// <returns></returns>
        public static bool KickoutUserBulk(long[] userIdCollection, string memo, DateTime? nbt, params string[] apps)
        {
            Dictionary<string, BanTokenData> keyValues = new Dictionary<string, BanTokenData>();
            foreach (var uid in userIdCollection)
            {
                var data = new BanTokenData { Banned = true, Memo = memo };
                data.NotBefore = nbt ?? null;
                if (apps != null && apps.Any())
                {
                    data.Apps.AddRange(apps);
                }
                keyValues.Add($"{KickoutCachePrefix}:uid:{uid}", data);
            }
            return Redis.MSet(keyValues, TimeSpan.FromDays(15));
        }
        /// <summary>
        /// 踢出用户缓存前缀
        /// </summary>
        public const string KickoutCachePrefix = "banned-jwt";
        /// <summary>
        /// 解除踢出用户
        /// </summary>
        /// <param name="userId"></param>
        public static void RemoveKickout(long userId)
        {
            Redis?.Delete($"{KickoutCachePrefix}:uid:{userId}");
        }
        internal static FreeRedisCache Redis;
        internal static MemoryCache Memory = new MemoryCache();
        /// <summary>
        /// 检查Token是否被禁用，true禁用，false未禁用
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="notBefore"></param>
        /// <param name="memo"></param>
        /// <param name="appid">应用ID</param>
        /// <returns></returns>
        internal static bool IsTokenBanned(string userId, string appid, DateTime? notBefore, out string memo)
        {
            string uid = Encoding.UTF8.GetString(Base58.Decode(userId.Substring(1)));
            BanTokenData cacheValue = GetString2rdCache($"{KickoutCachePrefix}:uid:{uid}");
            if (cacheValue == null || !cacheValue.Banned)
            {
                memo = null;
                return false;
            }

            //未指定的Apps，禁用全部应用token；若有指定应用，则只禁用颁发给此应用的token
            bool banApp = cacheValue.Apps == null || !cacheValue.Apps.Any() || cacheValue.Apps.Contains(appid);
            //未指定时间，禁用所有时间的token；如果有指定时间，则只禁用颁发时间早于此时间的token
            bool forceExpire = !cacheValue.NotBefore.HasValue || notBefore < cacheValue.NotBefore.Value;
            if (cacheValue.Banned && (!banApp || !forceExpire))
            {
                memo = null;
                return false;
            }
            memo = cacheValue.Memo;
            return cacheValue.Banned;
        }
        internal static BanTokenData GetString2rdCache(string key)
        {
            var value = Memory.Get<BanTokenData>(key);
            if (value != null)//不为 null 和 空字符串
            {
                return value;
            }
            if (value == null) //如果是 null
            {
                value = Redis.Get<BanTokenData>(key);
                Memory.Add(key, value ?? new BanTokenData(), TimeSpan.FromSeconds(300));
            }
            return value;
        }
        /// <summary>
        /// token颁发者
        /// </summary>
        public static string Issuer { get; set; }
        /// <summary>
        /// token接受者
        /// </summary>
        public static IEnumerable<string> Audiences { get; set; }
        /// <summary>
        /// token签名秘钥
        /// </summary>
        public static string Secret { get; set; }
        /// <summary>
        /// token错误信息
        /// </summary>
        public class TokenError
        {
            /// <summary>
            /// token错误信息
            /// </summary>
            /// <param name="headerName"></param>
            /// <param name="headerValue"></param>
            /// <param name="friendlyMsg"></param>
            public TokenError(string headerName, string headerValue, string friendlyMsg)
            {
                this.HeaderName = headerName;
                this.HeaderValue = headerValue;
                this.FriendlyMessage = friendlyMsg;
            }
            /// <summary>
            /// 响应头名称
            /// </summary>
            public string HeaderName { get; set; }
            /// <summary>
            /// 响应头值
            /// </summary>
            public string HeaderValue { get; set; }
            /// <summary>
            /// 友好信息
            /// </summary>
            public string FriendlyMessage { get; set; }
        }
    }
    /// <summary>
    /// 禁用token数据
    /// </summary>
    public class BanTokenData
    {
        /// <summary>
        /// 是否禁用，true禁用，false未禁用
        /// </summary>
        public bool Banned { get; set; }
        /// <summary>
        /// token颁发早于此时间的都禁用
        /// </summary>
        public DateTime? NotBefore { get; set; }
        /// <summary>
        /// 备注
        /// </summary>
        public string Memo { get; set; }
        /// <summary>
        /// 只在这些应用中禁用token
        /// </summary>
        public List<string> Apps { get; set; } = new List<string>();
    }
}
